在 Ubuntu18.04 安装 LEMP,构建本地网站

0x00 背景

1. 目的

基于 LEMP 栈 搭建本地 web 服务器,实现一个能在本地网络中访问的网页,网页中实现对数据库的简单操作。

学习目标:

  • 理解 web 服务器的运行,学会手动搭建一个 web 服务器
  • 学习简单的 HTML 和 PHP 知识,并编写网站前端和服务器端的代码
  • 学习 MySQL 数据库的基本知识,完成数据库的基本操作,实现 PHP 和 MySQL 的连接
  • 学习如何加固 LEMP 栈

2. LEMP 栈简介

LEMP 指代 Linux、Nginx、MySQL、PHP,是一个实现 web 服务器的栈,之所以简写为 LEMP 而不是 LNMP,因为Nginx 的读音同 Engine X,因此简写选的是 E 而不是 N,此外 LEMP 是实际可拼读的英文,而 LNMP 只能逐个字母发音(当然也有 LNMP 的简写,但个人比较支持 LEMP)。常见的搭建 web 服务器的组合还有 LAMP,它是 LEMP 的前辈, LAMP 中用的 web 服务器软件 为 Apache。

0x01 实现

1. 环境搭建过程

目标环境

linux+nginx+php- fpm+mysql

1.1 安装 Linux 系统

Linux 系统使用 Ubuntu-18.04 版本,使用 VMware Workstation 安装其镜像,Ubuntu 镜像在其官网上下载。

1
sudo apt-get update & sudo apt-get upgrade

1.2 安装 Nginx

在 Ubuntu 中安装 Nginx,只需要在终端输入以下命令即可:

1
sudo apt-get install nginx

在浏览器中浏览 nginx 默认网页,http://主机地址,说明安装成功

1.3 安装 Php-fpm

在终端输入以下命令即可:

1
sudo apt-get intall php-fpm

查看服务是否运行:

1
systemctl status php7.2-fpm.service

1.4 安装 Mysql

在终端输入以下命令安装 mysql 和 php 的 mysql 支持:

1
sudo apt-get install mysql-server mysql-client php-mysql

安装完成后,检查 mysql 服务器是否运行:

1
sudo systemctl mysql.service

2. 配置自己的网站

目标

搭建本地网站 www.jaylen.com,配置 Nginx 使其支持 php,本地网站显示一张图片,图片下方有个评论界面,可以输入评论,显示到网页中,评论会存到 mysql 数据库中。

2.1 域名映射

要在本地网络中,通过浏览器访问到 www.jaylen.com,需要修改主机的 hosts 文件,在 /etc/hsots 添加一行:

1
your-ip-add www.jaylen.com

这样就不需要 DNS 服务器解析该域名了(域名并没有注册!!!,DNS 服务器是没有记录的)。

2.2 配置 Nginx

首先需要配置 Nginx,使其支持 php,在 /etc/nginx/conf.d 目录下添加自己网站服务器的配置,jaylen.com.conf ,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#  server configuration
server {
listen 80;
listen [::]:80;
server_name jaylen.com;
root /var/www/jaylen.com; # 网站在主机的根目录

# Add index.php to the list if you are using PHP
index index.html index.htm index.nginx-debian.html index.php;

location / {
# First attempt to serve request as file, then
# as directory, then fall back to displaying a 404.
try_files $uri $uri/ =404;
}

# pass PHP scripts to FastCGI server
location ~ \.php$ {
include snippets/fastcgi-php.conf;

# With php-fpm (or other unix sockets):
fastcgi_pass unix:/run/php/php7.2-fpm.sock;
}

}

测试配置是否成功:

1
sudo nginx -t

成功的话,出现如下提示信息:

1
2
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

重新加载配置:

1
sudo nginx -s reload

2.3 创建网站主页

首先为网站创建一个根目录 /var/www/jaylen.com ,用于存储网站内容,该目录和 Nginx 配置文件中的 root 指令的内容一致。

1
sudo mkdir /var/www/jaylen.com

写个简单的 html 页面和 php进行测试,index.html

1
2
3
4
5
6
7
8
9
10
11
<html>
<body>
<head>
<title>
jaylen's homepage
</title>
</head>
<h1>Welcome to Jaylen's Homepage</h1>
<p>Website is under construction, wait...</p>
</bdoy>
</html>

index.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<html>
<body>
<head>
<title>
jaylen's homepage
</title>
</head>
<h1>Welcome to Jaylen's Homepage</h1>
<p>Website is under construction, wait...</p>
<?php
echo phpinfo();
?>
</bdoy>
</html>

通过浏览器浏览网站 www.jaylen.com

浏览 www.jaylen.com/index.php

浏览 www.jaylen.com/index.php

测试完成,没有问题。

进一步完善网页内容,使其显示一张图片,图片下方带有评论输入框,可以提交评论,提交的评论存到主机的 mysql 数据库中,而后显示在评论区,最后完成的大致如下图,简陋的不行。


在 mysql 中创建一个新用户,并授权 INSERT、SELECT 权限,评论内容需要存到数据库,并读取数据库内容。

1
2
3
4
5
6
7
mysql>
CREATE USER 'user101'@'localhost'
IDENTIFIED BY 'webdev101@Webdev102';
GRANT INSERT,SELECT
ON *.*
TO 'user101'@'localhost'
WITH GRANT OPTION;

刷新授权表,退出,

1
2
mysql> FLUSH PRIVILEGE;
mysql> quit;

以新创建的用户重新登陆

1
mysql -u user101 -p

创建一个数据库

1
mysql> create database webdb101

切换到该数据库

1
mysql> use webdb101

创建一个表格,用于存放评论内容

1
2
3
4
mysql> CREATE TABLE comments (
id INT(6) UNSIGNED AUTO_INCREMENT PRIMARY KEY,
comment VARCHAR(200) NOT NULL
);

插入一条数据

1
mysql> INSERT INTO comments(comment) VALUES ('test');

最终的 index.php 文件内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
<!DOCTYPE HTML>  
<html>
<head>
<style>
.error {color: #FF0000;}
img {
width: 30%;
height: auto;
}
td {
border: 1px solid black;
}
</style>
</head>
<body>

<?php
// define variables and set to empty values

include('connect-mysql.php');

$comment = "";
$commentErr = "";

if ($_SERVER["REQUEST_METHOD"] == "POST") {
if (empty($_POST["comment"])) {
$commentErr = "You must write someting...";
} else {
$comment = test_input($_POST["comment"]);
$sql = "INSERT INTO comments (comment) VALUES ('".$comment."')";
if (mysqli_query($dbcon, $sql)) {
$commentErr = "Submit comment successfully!";
}
else {
$commentErr = "Error:" . $sql . "<br>" . mysqli_error($dbcon);
}
}
}

function test_input($data) {
$data = trim($data);
$data = stripslashes($data);
$data = htmlspecialchars($data);
return $data;
}
?>

<h1>Jaylen's HomePage</h1>
<img src='image/homepage.jpeg' alt='homepage icon'>
<form method="post" action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]);?>">
<hr>
<p>Wirte your comment here!</p>
<textarea name="comment" rows="5" cols="80"><?php echo $comment;?></textarea>
<span class='error'>*<?php echo $commentErr; ?></span>
<br></br>
<input type="submit" name="submit" value="Submit">
<hr>
</form>
<?php
$sqlget = "SELECT * FROM comments";
$sqldata = mysqli_query($dbcon, $sqlget) or die("Fail to connect to database!" . mysqli_error($dbcon));

echo "<table>";
echo "<tr><th>Comments</th></tr>";

while($row = mysqli_fetch_array($sqldata, MYSQLI_ASSOC)) {
echo "<tr><td>";
echo $row['comment'];
echo "</td></tr>";
}

echo "</table>";
?>
</body>
</html>

连接数据库的操作,单独放到 connec-mysql.php 文件中。

在网页中测试下提交评论,

2.4 问题

记录搭建自己本地网络过程中遇到的问题。

  1. 配置 Nginx 使其支持 php 时,访问 http://localhsot/index.php,出现 502 Bad Gateway 错误,查看 Nginx 错误日志 /var/log/nginx/error.log ,找到错误记录如下:

    1
    2019/08/02 03:11:53 [crit] 4293#4293: *50 connect() to unix:/var/run/php/php7.0-fpm.sock failed (2: No such file or directory) while connecting to upstream, client: 192.168.47.129, server: _, request: "GET /index.php HTTP/1.1", upstream: "fastcgi://unix:/var/run/php/php7.0-fpm.sock:", host: "192.168.47.129"

    原因是找不到 unix:/var/run/php/php7.0-fpm.sock文件,而我安装的版本是 7.2 的,因此应该是配置错误了,在系统中查找 php7.0-fpm.sock 发现实际路径为 /run/php/php7.0-fpm.sock 修改 /etc/nginx/sites-avalable/default 文件如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    # pass PHP scripts to FastCGI server

    location ~ \.php$ {
    include snippets/fastcgi-php.conf;

    # With php-fpm (or other unix sockets):
    fastcgi_pass unix:/run/php/php7.2-fpm.sock;
    # With php-cgi (or other tcp sockets):
    #fastcgi_pass 127.0.0.1:9000;
    }
  2. index.php 文件中对数据库进行插入操作时,提示错误:

    1
    Unknown column '' in 'field list'

    原因时插入的数据是 VARCHAR 类型,需要额外添加双引号,我原来的 sql 语句如下:

    1
    $sql = "INSERT INTO comments (comment) VALUES ($comment)";

    这样实际导致 $comment 变量展开后是一个标识符,而不是字符串,需要在外面添加额外的双引号,具体处理如下,注意在单引号中 $ 不会被识别成特殊字符,因此不会展开变量,而双引号中不能直接包含双引号,所以就成了下面的结果。

    1
    $sql = "INSERT INTO comments (comment) VALUES ('".$comment."')";

3.本地服务器加固

3.1 加固 Linux 系统

时刻保持系统和软件为最新版本

在 Ubuntu 中检查系统需要更新的软件包,使用如下命令,apt-get -s 命令用于模拟后面命令的操作,但实际不会改变系统的状态,所以 apt-get -s upgrade 只会模拟软件更新的过程,你会看到被更新的软件的信息,但实际并没有更新到系统上 :

1
sudo apt-get update && sudo apt-get -s upgrade

然后根据需要更新你想要更新的软件。如果你想更新所有软件,使用如下命令:

1
sudo apt-get update && sudo apt-get upgrade

加固远程登陆

在使用和管理服务器时,往往我们需要远程登陆服务器,这就需要我们保证远程登陆过程的安全性。以下步骤一定程度提高了远程登陆的安全性。

  1. 强制使用高强度用户密码(数字、字母、字符的组合且长度14位以上)
  2. 更改 SSH 默认的端口(22)为随机端口
  3. 禁止 root 身份的远程登陆
  4. 使用公钥认证机制进行远程登陆
  5. 使用 Linux 标准用户而不是 root 用户执行上述操作,并且该用户的权限可提升成 root 权限

现以 Ubuntu 系统为例,完成上述操作:

强制使用高强度用户密码

要强制用户使用高强度密码,需要安装额外的模块 libpam-cracklib

1
sudo apt-get install libpam-cracklib

在 Ubuntu 中,密码策略(规定密码的长度,字符等)定义在 /etc/pam.d/common-password 文件中,如果要规定,密码长度为 14,包含大小写字符数字和字符,在文件中,在 pam_unix.so的前一行,添加:

1
password required pam_cracklib.so try_first_pass retry=3 minlen=14 lcredit=-1 ucredit=-1 dcredit=-1 ocredit=-1 difok=2 reject_username

上述配置的选项的描述如下,详情参考 libpam-cracklib 文档

选项 描述
retry=N 设置密码时的,最大重试次数
minlen=N 新密码的最小长度
lcredit=N 最少小写字母数,小于0,正常计算 minlen,大于0,计算 minlen 额外加 1
ucredt=N 最少大写字母数,小于0,正常计算 minlen,大于0,计算 minlen 额外加 1
dcredit=N 最少数字的数,小于0,正常计算 minlen,大于0,计算 minlen 额外加 1
ocredit=N 最少其它字符数,小于0,正常计算 minlen,大于0,计算 minlen 额外加 1
difok=N 和旧密码不同的字符数
reject_username 禁止用户名作为密码

==总之规定各种字符类型的个数,要使用负数,其绝对值表示该类型字符至少有多少个。==

更改 SSH 默认的端口(22)为随机端口

禁止 root 身份的远程登陆

修改 ssh-server 的配置文件:

1
sudo vim /etc/ssh/sshd_config

找到 # Port 22 更改为:

1
Port 2019

找到 #PermitRootLogin,更改为:

1
PermitRootLogin no

远程登陆则需要指定该端口执行登陆,而且无法使用 root 用户登陆

1
sss username@ip-addr -p 2019

使用公钥认证机制进行远程登陆

SSH 登陆 提供公钥认证机制的登陆,即不需要密码的登陆方式,但是需要客户端生成公钥和私钥,并将公钥发送给服务端,服务端将公钥添加到相应用户的配置文件中。

首先客户端需要生成,密钥对:

1
ssh-keygen -t rsa -b 4096

使用默认文件名,一路 Enter 最后,生成的密钥对文件在 ~/.ssh/ 目录下,其中 id_rsa 为私钥,id_rsa.pub 为公钥。

接着使用将公钥文件上传到服务器:

1
ssh-copy-id username@ip-addr -p portnum

该命令会把公钥文件的内容,写入到 /home/username/ssh/authorized_keys 文件中,所以也可以手动添加内容。如此一来就可以 username 的身份,不使用密码登陆服务器了。

1
ssh -p 2019 username@ip-addr

3.2 加固 Nginx

防止信息泄露

Nginx 默认开启 Server Token(显示版本号),这样使得 Nginx 的版本号很容易被获取,如下图为连接域名不存在资源时的返回页面,可以看到 Nginx 的版本号


/etc/nginx/nginx.conf 中 http 块中添加(去掉注释即可):

1
server_tokens off

关闭后,访问域名下不存在的资源,返回页面中没有了 Nginx 的版本号 信息。

增加访问控制策略

Nginx 可以使用 allowdeny 指令在配置文件中允许或禁止特定 IP 的访问, 编辑 Niginx 配置文件 /etc/nginx/conf.d/jaylen.com.conf,只允许 192.168.47.129 192.168.47.130 访问 网站 www.jaylen.com

1
2
3
4
5
6
7
8
9
10
11
#  server configuration
server {
listen 80;
listen [::]:80;

# IPs access control
allow 192.168.47.129;
allow 192.168.47.130;
deny all;

root /var/www/jaylen.com;

使用 TLS 加固 Nginx

TLS 可以加密客户端和服务端通信的数据,降低信息泄露的风险。对于本地网站可以使用 SSL 自签 证书 实现 HTTPS 连接。当然在公网中使用的网站,通常会使用 CA 认证的证书,要免费使用 SSL 证书,可参考:

How to Install Nginx with Let’s encrypt and get A+ from SSLLabs Test

要在 Nginx 配置自签证书,首先在配置目录下建一个文件夹,进到文件夹中:

1
2
sudo mkdir /etc/nginx/ssl/
cd /etc/nginx/ssl

生成密钥:

1
sudo openssl genrsa -aes256 -out nginx.key 1024

接着生成 CSR

1
sudo openssl req -new -key nginx.key -out nginx.csr
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Enter pass phrase for nginx.key:
Can't load /home/jaylen/.rnd into RNG
140067713049024:error:2406F079:random number generator:RAND_load_file:Cannot open file:../crypto/rand/randfile.c:88:Filename=/home/jaylen/.rnd
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:CN
State or Province Name (full name) [Some-State]:Guangdong
Locality Name (eg, city) []:Guangzhou
Organization Name (eg, company) [Internet Widgits Pty Ltd]:IT
Organizational Unit Name (eg, section) []:IT
Common Name (e.g. server FQDN or YOUR name) []:www.jaylen.com
Email Address []:jaylen@gmail.com

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:admin
An optional company name []:IT

最后,签发证书:

1
sudo openssl x509 -req -days 365 -in nginx.csr -signkey nginx.key -out nginx.crt

成功签发:

1
2
3
4
5
ignkey nginx.key -out nginx.crt
Signature ok
subject=C = CN, ST = Guangdong, L = Guangzhou, O = IT, OU = IT, CN = www.jaylen.com, emailAddress = jaylen@gmail.com
Getting Private key
Enter pass phrase for nginx.key:

接着在 /etc/nginx/conf.d/jaylen.com.conf 中修改:

1
2
3
4
5
server {
listen 443 ssl;
ssl_certificate /etc/nginx/ssl/nginx.crt;
ssl_certificate_key /etc/nginx/ssl/nginx.key;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;

重新加载 Nginx 配置:

1
sudo nginx -s reload

提示输入之前步骤设置的密码,输入即可。

其它 加固措施,可仔细阅读:

Top 25 Nginx Web Server Best Security Practices

Nginx Web Server Security and Hardening Guide

3.3 加固 mysql

运行 mysql_secure_installation 工具(安装 mysql 后自带的 shell 脚本),进行 mysql 的安全检查,

根据提示,设置密码为最高级别,并为 root 用户设置密码,最后同意以下选项:

  • Remove anonymous users? – 删除匿名用户
  • Disallow root login remotely? – 禁止远程使用 root 用户登陆
  • Remove test database and access to it? – 删除测试数据库和其访问权限
  • Reload privilege tables now? – 重载授权表

3.4 加固 PHP

修改 php.ini 文件

当文件不存在时停止 PHP 处理

Nginx 对于 PHP 支持的配置文件中常常会使用如下形式的配置,该配置使得 PHP 解释器接受所有以 .php结尾的 URI,这样一来就会存在很大的风险,存在任意代码执行漏洞,具体解释见参考资料 [6]。

修改 /etc/php/7.2/fpm/php.ini文件(不同系统该文件位置略有不同),设置cgi.fix_pathinfo=0,可以禁止 PHP 解释器查找文件系统中不存在的文件,使用sed 命令完成文件内容的修改:

1
sudo sed -i 's/;cgi.fix_pathinfo=1/cgi.fix_pathinfo=0/g' /etc/php/7.2/fpm/php.ini

禁用危险的 PHP 函数

php.ini 中添加:

1
disable_functions =exec,eval,phpinfo,passthru,shell_exec,system,proc_open,popen,curl_exec,curl_multi_exec,parse_ini_file,show_source

限制文件上传功能

如果网站不需要文件上传功能,应该禁用。

1
file_uploads=Off

如果需要上传功能,则设置文件的大小,根据实际情况设置,如头像图片上传1M足矣。

1
2
file_uploads=On
upload_max_filesize=1M

设置 POST 方法传输数据的大小

POST 方法是当客户端需要向服务器发送数据时使用的,该方法可被用于对服务器进行 DoS 攻击等,所以需要将其能传输的数据大小设置成合理的数值,如果网站不需要上传文件等数据量大的操作,4KB也应该足够了。

1
post_max_size=1K

防止 PHP 信息泄露

1
expose_php = Off

限制 PHP 脚本的最长执行时间

1
2
3
4
# set in seconds
max_execution_time = 30 #最长执行时间 30 s
max_input_time = 30 #脚本解析输入最长时间30s
memory_limit = 40M #脚本最大使用内存40M

这样可以有效防止大规模的 DOS 攻击。

禁用未使用的 PHP 模块

查看已安装的 PHP 模块:

1
php -m

根据实际情况,禁用不使用模块,注释掉 php.ini相应的配置行。

0x02 参考资料

[1] Install a LEMP Stack on Ubuntu 18.04

[2] Serve PHP with PHP-FPM and NGINX

[3] Nginx vs Apache

[4] Setting up an Nginx Reverse Proxy

[5] Getting Started with NGINX

[6] Passing Uncontrolled Requests to PHP

[7] Unknown column '' in 'field list'解决方案

[8] What’s a LEMP stack?

[9] How to secure LEMP stack

[10] Use Public Key Authentication with SSH

[11] Top 25 Nginx Web Server Best Security Practices

[12] Nginx Web Server Security and Hardening Guide

文章作者: BingSlient
文章链接: https://bingslient.github.io/2019/10/31/本地 web 服务器搭建之LEMP/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 BingSlient's Blog
打赏
  • 微信
  • 支付寶